Skip to content

Conversation

@YuanboXue-Amber
Copy link
Contributor

@YuanboXue-Amber YuanboXue-Amber commented Aug 23, 2023

Fix issue 1+2 in #28938

Previous Behavior

In the codesandbox in the original issue: https://codesandbox.io/s/inspiring-fast-5jm9wc?file=/example.tsx
Drag scrollbar in the scrollable container does not close the popover.
Click trigger to open popover, keep mouse pointer within trigger and scroll away does not close the popover.

New Behavior

I updated the useOnScrollOutside to listen to scroll so it can close the popover in both cases.

@YuanboXue-Amber YuanboXue-Amber changed the title fix(react-utilities): click scrollbar should invoke callback in useOnClickOutside fix(react-utilities): drag scrollbar should invoke callback in useOnScrollOutside Aug 23, 2023
@codesandbox-ci
Copy link

codesandbox-ci bot commented Aug 23, 2023

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 3320a87:

Sandbox Source
@fluentui/react 8 starter Configuration
@fluentui/react-components 9 starter Configuration
compassionate-river-499vz3 PR

@fabricteam
Copy link
Collaborator

fabricteam commented Aug 23, 2023

Perf Analysis (@fluentui/react-components)

Scenario Render type Master Ticks PR Ticks Iterations Status
FluentProviderWithTheme virtual-rerender-with-unmount 79 76 10 Possible regression
All results

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 609 617 5000
Button mount 317 317 5000
Field mount 1105 1128 5000
FluentProvider mount 690 693 5000
FluentProviderWithTheme mount 84 82 10
FluentProviderWithTheme virtual-rerender 65 64 10
FluentProviderWithTheme virtual-rerender-with-unmount 79 76 10 Possible regression
InfoButton mount 16 14 5000
MakeStyles mount 859 852 50000
Persona mount 1727 1641 5000
SpinButton mount 1379 1333 5000

@fabricteam
Copy link
Collaborator

fabricteam commented Aug 23, 2023

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
203.722 kB
57.915 kB
203.811 kB
57.928 kB
89 B
13 B
react-datepicker-compat
DatePicker Compat
207.261 kB
57.469 kB
207.35 kB
57.482 kB
89 B
13 B
react-infobutton
InfoButton
125.517 kB
39.389 kB
125.606 kB
39.404 kB
89 B
15 B
react-infobutton
InfoLabel
129.188 kB
40.559 kB
129.277 kB
40.575 kB
89 B
16 B
react-menu
Menu (including children components)
135.626 kB
41.648 kB
135.715 kB
41.666 kB
89 B
18 B
react-menu
Menu (including selectable components)
138.312 kB
42.154 kB
138.401 kB
42.172 kB
89 B
18 B
react-popover
Popover
114.788 kB
36.02 kB
114.877 kB
36.037 kB
89 B
17 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
global-context
createContext
510 B
330 B
global-context
createContextSelector
537 B
342 B
react-accordion
Accordion (including children components)
88.688 kB
26.917 kB
react-alert
Alert
81.668 kB
21.964 kB
react-avatar
Avatar
47.01 kB
14.502 kB
react-avatar
AvatarGroup
16.116 kB
6.431 kB
react-avatar
AvatarGroupItem
61.789 kB
18.912 kB
react-badge
Badge
23.183 kB
7.254 kB
react-badge
CounterBadge
24.084 kB
7.553 kB
react-badge
PresenceBadge
22.14 kB
7.867 kB
react-button
Button
36.91 kB
9.685 kB
react-button
CompoundButton
44.259 kB
11.167 kB
react-button
MenuButton
41.296 kB
10.933 kB
react-button
SplitButton
49.331 kB
12.486 kB
react-button
ToggleButton
53.956 kB
11.582 kB
react-card
Card - All
88.334 kB
25.252 kB
react-card
Card
83.177 kB
23.742 kB
react-card
CardFooter
9.338 kB
3.932 kB
react-card
CardHeader
11.589 kB
4.707 kB
react-card
CardPreview
10.297 kB
4.317 kB
react-checkbox
Checkbox
32.872 kB
10.652 kB
react-combobox
Combobox (including child components)
87.101 kB
28.273 kB
react-combobox
Dropdown (including child components)
85.555 kB
27.926 kB
react-components
react-components: Button, FluentProvider & webLightTheme
66.583 kB
18.469 kB
react-components
react-components: FluentProvider & webLightTheme
37.926 kB
12.419 kB
react-dialog
Dialog (including children components)
86.738 kB
26.341 kB
react-divider
Divider
17.098 kB
6.303 kB
react-field
Field
18.295 kB
6.978 kB
react-image
Image
12.029 kB
4.783 kB
react-input
Input
23.237 kB
7.688 kB
react-jsx-runtime
Classic Pragma
1.049 kB
531 B
react-jsx-runtime
JSX Dev Runtime
2.752 kB
1.277 kB
react-jsx-runtime
JSX Runtime
3.27 kB
1.519 kB
react-label
Label
10.423 kB
4.31 kB
react-link
Link
13.254 kB
5.397 kB
react-overflow
hooks only
12.419 kB
4.687 kB
react-persona
Persona
53.905 kB
16.371 kB
react-portal
Portal
12.255 kB
4.504 kB
react-portal-compat
PortalCompatProvider
6.48 kB
2.203 kB
react-positioning
usePositioning
25.137 kB
9.109 kB
react-progress
ProgressBar
13.718 kB
5.476 kB
react-provider
FluentProvider
18.471 kB
6.847 kB
react-radio
Radio
26.617 kB
8.601 kB
react-radio
RadioGroup
11.622 kB
4.833 kB
react-select
Select
24.599 kB
8.651 kB
react-slider
Slider
34.134 kB
11.039 kB
react-spinbutton
SpinButton
32.773 kB
10.216 kB
react-spinner
Spinner
19.678 kB
7.03 kB
react-switch
Switch
29.092 kB
9.217 kB
react-table
DataGrid
152.991 kB
42.325 kB
react-table
Table (Primitives only)
39.638 kB
12.129 kB
react-table
Table as DataGrid
126.247 kB
33.617 kB
react-table
Table (Selection only)
71.617 kB
18.915 kB
react-table
Table (Sort only)
70.236 kB
18.516 kB
react-tags-preview
InteractionTag
11.031 kB
4.528 kB
react-tags-preview
Tag
27.792 kB
8.768 kB
react-tags-preview
TagGroup
69.626 kB
20.485 kB
react-text
Text - Default
13.043 kB
5.129 kB
react-text
Text - Wrappers
16.215 kB
5.447 kB
react-textarea
Textarea
27.302 kB
9.047 kB
react-toast
Toast (including Toaster)
87.733 kB
26.042 kB
react-tooltip
Tooltip
48.1 kB
16.875 kB
react-utilities
SSRProvider
180 B
159 B
🤖 This report was generated against 5d2f8b98de08a514bd5b8fa0f3d69adf4e6f426d

@fabricteam
Copy link
Collaborator

fabricteam commented Aug 23, 2023

🕵 fluentuiv9 No visual regressions between this PR and main

@size-auditor
Copy link

size-auditor bot commented Aug 23, 2023

Asset size changes

Size Auditor did not detect a change in bundle size for any component!

Baseline commit: 5d2f8b98de08a514bd5b8fa0f3d69adf4e6f426d (build)

@YuanboXue-Amber YuanboXue-Amber marked this pull request as ready for review August 24, 2023 08:32
@YuanboXue-Amber YuanboXue-Amber requested a review from a team as a code owner August 24, 2023 08:32
@YuanboXue-Amber YuanboXue-Amber requested a review from a team as a code owner August 29, 2023 10:09
@ling1726 ling1726 removed the request for review from a team August 29, 2023 11:02
@YuanboXue-Amber
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@YuanboXue-Amber YuanboXue-Amber requested review from a team and sopranopillow as code owners August 29, 2023 13:09
@YuanboXue-Amber
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@YuanboXue-Amber YuanboXue-Amber enabled auto-merge (squash) August 30, 2023 08:29
@YuanboXue-Amber
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

Comment on lines 211 to 215
export type OpenPopoverEvents =
| Event
| MouseEvent
| TouchEvent
| React.FocusEvent<HTMLElement>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I think this is a breaking change. Existing onOpenChange handlers aren't prepared to handle events other than MouseEvent, TouchEvent, or FocusEvent. The only way I can think of to make this not breaking is to create an onOpenChange2 event with the new type for the event arg.

Copy link
Contributor

@ling1726 ling1726 Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good catch 👍, I tried playing with this a bit more in TS playground and it can indeed break typescript build depending on how users define their onOpenChange handlers https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcDyKB2AFCkBuwUAooZjAM5wC8AUHHAD5ynDn1NwCyEArhcFbsGzACp8AxgAshMWvKQo42KHirU4Abw4MImDGwDCUgIaYA5sABccABTX0WXASKyKAShoA+OPggBLABMAbloAX3kJPQp4PQNMYzNLGjsHHn5BMngxSRksz2ofbXDI6PgwVTAKGxU1FM04OKxEi1Qw0NogA

This is actually quite worrying because we will never be able to add new events to our callback types without breaking users 😱 - this looks like a good candidate to discuss in tech sync with the wider team

Copy link
Contributor

@ling1726 ling1726 Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case we might want to consider casting internally in the Popover and Menu components to one of the existing event types, to unblock the fixing of the bugs in the description. It will change the runtime behaviour, but I think that is acceptable compared to the risk of build breaks for users.

However actually adding new events to types in useOnScrollOutside is also technically a breaking change in that case, even if the utility is not exported by our suite package 😱😱 - if we want full non-break coverage we should probably revert to @YuanboXue-Amber's original implementation and even cast the new event type within the utility.

All options are pretty bad for me, WDYT @behowell?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original implementation is at this commit: b73b2bd

Copy link
Contributor

@behowell behowell Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting to avoid a breaking change in types is still a breaking change; and possibly even worse: it results in extra-confusing runtime bugs when the object isn't the type that was promised. In this case, the event object would be missing any of the properties that are in MouseEvents.

I think in practice, it's pretty unlikely that it will cause an actual problem in this case, especially since the existing type includes React.FocusEvent<HTMLElement> and already requires you to do some runtime inspection of the object to see what type it is.

It might be worth discussing this with the rest of the team to come up with the right fix here. This isn't the first time this has caused a problem. In my recent PR #28951, I had to come up with a "hacky" workaround to this problem, by including the new event on the data argument instead. Unfortunately, that won't work here because it requires passing undefined for the event argument, which would be a breaking change here (the OpenPopoverEvents type doesn't include undefined as a possibility).

All of the possible options I can think of are broken in some way, so we'd have to decide what the least-bad option is:

  1. Add | Event to the OpenPopoverEvents type (this PR).
    • Pros: Gets the type correct. Any potential breaks are caught at build time.
    • Cons: It's a breaking change for any code that explicitly types its event argument (instead of using our exported OpenPopoverEvents type).
  2. Cast the event type to MouseEvent.
    • Pros: No type changes, and no changes to existing code to listen to the new case where onOpenChange can be called.
    • Cons: Still a breaking change, but the break happens at runtime instead of build time. The type is a lie.
  3. Create onOpenChange2 with the new types, and deprecate onOpenChange.
    • Pros: No type breaks to existing code.
    • Cons: Breaks the contract of the old onOpenChange: it won't be called when the popover hides in response to a scrollbar. Code will need to be updated to onOpenChange2. Also, this adds a bunch of code to keep both the old event and new event working.

Given all of that, I think option 1 might actually be the least-bad option.

Copy link
Contributor

@ling1726 ling1726 Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that 1. might be the least bad option, but it would require discussion with the wider team to make sure we know about it and the consequences. I would propose that we:

  1. Implement option 2 (casting the event) to fix the standing bug
  2. create an issue to track whether we stick to option 2 or switch to option 1
  3. discuss the new issue in tech sync

In the interim option 2 is the best case IMO, since build breaks generally block our users from upgrading to fix other issues. I can't as easily think of cases where users rely on knowing exactly what the event type is. However for option 1 it's quite common from what I see in codebases to type the event

// 👍 no need to do anything
const onOpenChange: PopoverProps['onOpenChange'] = (e, data) => {/**/}

// 👎 breaking change - I've generally seen this more often since developers generally don't pick from PopoverProps as much
const onOpenChange = (e: React.MouseEvent | React.FocusEvent, data: PopoverOpenData) => {/**/}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a new PR #29062 using the cast option (option 2) to fix the original issue. Let's discuss how to extend event type this wednesday

Comment on lines +5 to +10
export type UseOnScrollOutsideOptions = Pick<UseOnClickOutsideOptions, 'element' | 'refs' | 'contains' | 'disabled'> & {
/**
* Called if the scroll is outside the element refs
*/
callback: (ev: Event | MouseEvent | TouchEvent) => void;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have /** @internal */ since it appears to only be used by an internal utility?

Copy link
Contributor

@ling1726 ling1726 Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add it, but I'm not sure what the use of @internal is anymore, even if these utilties are internal we are still bound by the requirement to never break them until the next major, now that our API surface includes our entire set of packages 🥲

import type { UseOnClickOrScrollOutsideOptions } from './useOnClickOutside';
import { UseOnClickOutsideOptions } from './useOnClickOutside';

export type UseOnScrollOutsideOptions = Pick<UseOnClickOutsideOptions, 'element' | 'refs' | 'contains' | 'disabled'> & {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, the goal here is to use UseOnClickOutsideOptions, except replace the callback field? If so, it'd be better to use Omit instead of Pick:

Suggested change
export type UseOnScrollOutsideOptions = Pick<UseOnClickOutsideOptions, 'element' | 'refs' | 'contains' | 'disabled'> & {
export type UseOnScrollOutsideOptions = Omit<UseOnClickOutsideOptions, 'callback'> & {

Copy link
Contributor

@ling1726 ling1726 Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd personally prefer Pick since there's less risk of picking up any new properties unintentionally in the future. Since it's only types, there should be no consequences the extra code to pick explicit properties.

Pick will also break if any properties are removed - which Omit will not do https://www.typescriptlang.org/play?#code/C4TwDgpgBAYg9nAvFA3gKCpqAjAhgJwC4oA7AVwFtsJ8BuDLPAL2OwQBsJcT6BfNNKEhQAQgSjIA8hQCWwADzw4AGigByZmoB89QeGhimEqAAUZAYwDWihKo0E1UAD7qAZgm31MQA

@YuanboXue-Amber YuanboXue-Amber changed the title fix(react-utilities): drag scrollbar should invoke callback in useOnScrollOutside fix(react-utilities): drag scrollbar should invoke callback in useOnScrollOutside with extending event type Sep 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants